﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Collections.ObjectModel;
using Pfz.Factoring;
using System.Windows.Input;
using Pfz.Extensions;
using System.Windows.Media;

namespace Pfz.WpfControls
{
	/// <summary>
	/// Control that presents other controls as "Modal", so it only returns when a result is given.
	/// </summary>
	public class ModalPresenter:
		UserControl
	{
		/// <summary>
		/// The RoutedEvent for ControlPresented.
		/// </summary>
		public static readonly RoutedEvent ElementPresentedEvent =
			EventManager.RegisterRoutedEvent
			(
				"ElementPresented",
				RoutingStrategy.Direct,
				typeof(EventHandler<ElementPresentedEventArgs>),
				typeof(ModalPresenter)
			);

		/// <summary>
		/// The RoutedEvent for ControlClosed.
		/// </summary>
		public static readonly RoutedEvent ElementClosedEvent =
			EventManager.RegisterRoutedEvent
			(
				"ElementClosed",
				RoutingStrategy.Direct,
				typeof(EventHandler<ElementClosedEventArgs>),
				typeof(ModalPresenter)
			);

		private List<ModalPresenterFrame> _frames = new List<ModalPresenterFrame>();

		/// <summary>
		/// Creates a new ModalPresenter instance.
		/// </summary>
		public ModalPresenter()
		{
			_children = new ModalPresenterChildren(_frames);
		}

		private ModalPresenterChildren _children;
		/// <summary>
		/// Gets the Children collection.
		/// </summary>
		public ModalPresenterChildren Children
		{
			get
			{
				return _children;
			}
		}

		private SameAreaPanel _panel;
		/// <summary>
		/// Gets or sets a value indicating if all elements are shown (using LastShowsFirstPanel) or if only the last
		/// one must be shown.
		/// </summary>
		public bool MustPresentAllElements
		{
			get
			{
				return _panel != null;
			}
			set
			{
				if (value == MustPresentAllElements)
					return;

				if (value)
				{
					_panel = new SameAreaPanel();
					base.Content = _panel;

					var panelChildren = _panel.Children;
					foreach(var child in _frames)
						panelChildren.Add(child._element);
				}
				else
				{
					_panel.Children.Clear();
					_panel = null;

					int count = _frames.Count;
					if (count == 0)
						base.Content = null;
					else
						base.Content = _frames[count-1]._element;
				}
			}
		}

		/// <summary>
		/// When the Parent changes, closes all presented controls.
		/// </summary>
		protected override void OnVisualParentChanged(DependencyObject oldParent)
		{
			base.OnVisualParentChanged(oldParent);

			CloseAll();
		}

		/// <summary>
		/// Closes the actual element or throws an exception.
		/// </summary>
		public void Close(object result)
		{
			if (!TryClose(result))
				throw new InvalidOperationException("Can't close a ModalPresenter that is not presenting anything.");
		}

		/// <summary>
		/// Tries set a result for the actual control being presented.
		/// If there is no control, returns false.
		/// </summary>
		public bool TryClose(object result)
		{
			int count = _frames.Count;
			if (count == 0)
				return false;

			var frame = _frames[count-1];
			frame._dispatcherFrame.Continue = false;
			frame._result = result;

			int index = count-1;
			_frames.RemoveAt(index);

			if (_panel != null)
				_panel.Children.RemoveAt(index);
			else
			{
				if (index == 0)
					base.Content = null;
				else
					base.Content = _frames[index-1]._element;
			}

			return true;
		}

		/// <summary>
		/// Closes all the presented controls.
		/// </summary>
		public void CloseAll()
		{
			foreach(var frame in _frames)
				frame._dispatcherFrame.Continue = false;

			_frames.Clear();

			if (_panel != null)
				_panel.Children.Clear();
			else
				base.Content = null;
		}

		/// <summary>
		/// Presents the given element as modal. Will wait until it returns.
		/// </summary>
		public object PresentModal(UIElement element)
		{
			if (element == null)
				throw new ArgumentNullException("element");

			var focus = Keyboard.FocusedElement as FrameworkElement;
			if (focus != null)
			{
				var parent = focus.Parent as FrameworkElement;
				while(parent != this)
				{
					if (parent == null)
					{
						focus = null;
						break;
					}

					parent = parent.Parent as FrameworkElement;
				}
			}

			if (_panel != null)
				_panel.Children.Add(element);
			else
				base.Content = element;

			ModalPresenterFrame frame;
			try
			{
				var dispatcherFrame = new DispatcherFrame();
				frame = new ModalPresenterFrame();
				frame._element = element;
				frame._dispatcherFrame = dispatcherFrame;

				_frames.Add(frame);

				try
				{
					ElementPresentedEventArgs presentedArgs = new ElementPresentedEventArgs(ElementPresentedEvent);
					presentedArgs.Element = element;
					RaiseEvent(presentedArgs);

					using(new ResultSetter(Close))
						Dispatcher.PushFrame(dispatcherFrame);
				}
				catch
				{
					_frames.Remove(frame);
					throw;
				}
			}
			catch
			{
				if (_panel != null)
					_panel.Children.Remove(element);
				else
					base.Content = null;

				throw;
			}

			if (focus != null)
			{
				Dispatcher.BeginInvoke
				(
					new Action
					(
						() => focus.Focus()
					),
					DispatcherPriority.Input
				);
			}

			ElementClosedEventArgs closedArgs = new ElementClosedEventArgs(ElementClosedEvent);
			closedArgs.Element = element;
			closedArgs.Result = frame._result;
			RaiseEvent(closedArgs);

			return frame._result;
		}

		/// <summary>
		/// Sets the result for a given element.
		/// If that element doesn't exist, throws an exception.
		/// </summary>
		public void Close(UIElement element, object result)
		{
			if (!TryClose(element, result))
				throw new ArgumentException("element is not in the actual frame list.");
		}

		/// <summary>
		/// Tries to set a result to the given element.
		/// If the element doesn't exist, returns false.
		/// </summary>
		public bool TryClose(UIElement element, object result)
		{
			if (element == null)
				return false;

			int count = _frames.Count;
			for(int i=0; i<count; i++)
			{
				var frame = _frames[i];
				if (frame._element == element)
				{
					frame._result = result;

					for(int j=i; j<count; j++)
						_frames[j]._dispatcherFrame.Continue = false;

					_frames.RemoveRange(i, count-i);

					if (_panel != null)
						_panel.Children.RemoveRange(i, count-i);
					else
					{
						if (i == 0)
							base.Content = null;
						else
							base.Content = _frames[i-1]._element;
					}

					return true;
				}
			}

			return false;
		}

		/// <summary>
		/// Tries to set a ModalPresenter result over the given framework element.
		/// It will search in all parents to see if one is a ModalPresenter. Returns true if it
		/// finds and sets the result, false otherwise.
		/// </summary>
		public static bool StaticTryClose(FrameworkElement element, object result)
		{
			if (element == null)
				throw new ArgumentNullException("element");
			
			var parent = element.Parent;
			while(parent != null)
			{
				ModalPresenter presenter = parent as ModalPresenter;
				if (presenter != null)
					if (presenter.TryClose(element, result))
						return true;

				FrameworkElement frameworkElement = element as FrameworkElement;
				if (frameworkElement == null)
					break;

				parent = frameworkElement.Parent;
			}

			return false;
		}

		/// <summary>
		/// Event invoked when a new element is presented.
		/// </summary>
		public event EventHandler<ElementPresentedEventArgs> ElementPresented
		{
			add
			{
				AddHandler(ElementPresentedEvent, value);
			}
			remove
			{
				RemoveHandler(ElementPresentedEvent, value);
			}
		}

		/// <summary>
		/// Event invoked when a modal element is closed.
		/// </summary>
		public event EventHandler<ElementClosedEventArgs> ElementClosed
		{
			add
			{
				AddHandler(ElementClosedEvent, value);
			}
			remove
			{
				RemoveHandler(ElementClosedEvent, value);
			}
		}

		/// <summary>
		/// Gets the actual ModalElement.
		/// </summary>
		public UIElement ModalElement
		{
			get
			{
				var children = Children;
				int count = children.Count;
				if (count == 0)
					return null;

				return children[count-1];
			}
		}
	}
}
